package com.filldream.fastboot.web.config.request;

import com.alibaba.fastjson2.JSONObject;
import com.filldream.fastboot.base.entity.SystemUser;
import com.filldream.fastboot.base.mapper.SystemUserMapper;
import com.filldream.fastboot.base.response.app.AccountManagerDTO;
import com.filldream.fastboot.base.response.pc.SystemLoginDTO;
import com.filldream.fastboot.common.annotation.PassTokenAnnotation;
import com.filldream.fastboot.common.annotation.RolePermissions;
import com.filldream.fastboot.common.constant.FinalStr;
import com.filldream.fastboot.common.entity.B;
import com.filldream.fastboot.common.entity.ErrorCode;
import com.filldream.fastboot.common.entity.RequestDTO;
import com.filldream.fastboot.common.exception.GlobalException;
import com.filldream.fastboot.common.util.*;
import com.filldream.fastboot.core.LoginHolder;
import com.filldream.fastboot.core.cache.CacheUtil;
import com.filldream.fastboot.core.manager.AccountManager;
import com.wechat.pay.java.core.http.MediaType;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;

/**
 * 请求拦截器
 * 功能：打印请求日志、鉴权
 * @author RickSun
 * @data 2020-10-22 11:04
 * @updata 2023-12-08 13:40
 */
@Service
@Slf4j
@Component
public class MethodFilterConfig implements HandlerInterceptor {

    @Autowired
    private AccountManager accountManager;

    @Autowired
    private SystemUserMapper systemUserMapper;

    @Value("${project.safe.test}")
    private boolean isAppTest;

    @Value("${project.safe.pctest}")
    private boolean isPcTest;

    @Value("${project.safe.pass_token_enable}")
    private boolean isPass;

    private final PathMatcher matcher = new AntPathMatcher();

    //请在此配置不需要拦截的方法地址
    private final List<String> excludePathPatterns = Arrays.asList(
            "/pc/systemUser/login","/app/user/login","/app/user/wxLogin","/app/user/accountLogin", "/app/user/forgetPwd", "/app/user/register"
    );

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {

        //生成RequestId
        String requestId = OrderSnUtil.genLogNo();
        MDC.put(FinalStr.requestId, requestId);

        //预处理直接请求通过
        String methodType = request.getMethod();
        if( methodType.equals("OPTIONS") || methodType.equals("HEAD") ){
            response.setStatus(HttpServletResponse.SC_OK);
            //预请求一律返回给前端200，但是后端要拦截下来，用户可按需更改
            return false;
        }

        //获取method
        HandlerMethod handlerMethod;
        Method method = null;
        try {
            handlerMethod = (HandlerMethod) handler;
            method = handlerMethod.getMethod();
        } catch (Exception e) {
            log.error("错误路径：{}", request.getRequestURI());
            log.error("类型强制转换异常：{}", e.getMessage());
            res404(response);
            e.printStackTrace();
            return false;
        }

        //打印并输出日志
        RequestDTO requestLog = RequestUtil.getRequestLog(request,method);
        log.info("*****RequestLog:{} {}:{} {}{} Head:{} IP:{}",
                requestLog.getTitle(),
                requestLog.getMethodType(),
                requestLog.getUri(),
                requestLog.getQueryParam(),
                requestLog.getBody().replaceAll("\t",""),
                requestLog.getHeadMap(),
                requestLog.getIp());

        //处理不需要走业务逻辑的地址,如果满足以下配置路径，则跳过该拦截器
        if( dealExcludePathPatterns(requestLog.getUri()) ){
            return true;
        }

        //------------开始处理鉴权等业务-------------

        //处理PassToken是否正确
        PassTokenAnnotation passTokenAnnotation = RequestUtil.getPassTokenAnnotation(method,isPass);
        if( passTokenAnnotation != null ){
            new PassTokenVerify().checkTokenPart( request.getHeader(FinalStr.passToken),requestLog );
        }

        //请求上下文，即server.servlet.context-path配置
        String contextPath = request.getContextPath();
        //请求相对路径
        String uri = requestLog.getUri();
        //如果是公用接口，则一律返回true
        if( uri.indexOf(contextPath + "/common/") == 0 ){
            return true;
        }

        //校验是否是APP或PC请求
        boolean isApp = uri.indexOf(contextPath + "/app/") == 0;
        boolean isPc = uri.indexOf(contextPath + "/pc/") == 0;

        //获取token判断token是否是纯数字
        String token = request.getHeader("token");
        boolean tokenIsNumber = CheckUtil.checkArabicNumerals(token);

        if( isApp ){
            dealAppRequest(token,tokenIsNumber);
            return true;
        }else if( isPc ){
            dealPcRequest(token,tokenIsNumber,method);
            return true;
        }else{
            throw new GlobalException(ErrorCode.NO_LOGIN.reMsg());
        }
        //------------结束处理鉴权等业务-------------
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
            throws Exception {
        LoginHolder.remove();
    }


    //----------辅助方法----------

    /**
     * 校验APP端请求
     * @param token
     * @param tokenIsNumber
     * @return
     */
    public void dealAppRequest(String token,boolean tokenIsNumber){
        //不管是否测试环境，APP前端接口如果没有token一律报未登录
        Assert.isEmpty(token,ErrorCode.NO_LOGIN.reMsg());
        AccountManagerDTO accountManagerDTO = null;

        //测试环境,token为用户ID
        if( isAppTest && tokenIsNumber ){
            accountManagerDTO = accountManager.selectById(Long.valueOf(token));
        }
        if(  ( isAppTest && tokenIsNumber ==false ) || isAppTest == false ){
            //非测试环境
            Long accountId = CacheUtil.getIdByToken();
            Assert.isEmpty(accountId,ErrorCode.NO_LOGIN.reMsg());
            accountManagerDTO = accountManager.selectById(accountId);
        }
        Assert.isEmpty(accountManagerDTO,ErrorCode.NO_LOGIN.reMsg());
        LoginHolder.setUserInfo(accountManagerDTO);
    }

    /**
     * 校验PC端请求
     * 处理逻辑：
     * 1、RolePermissions不为空,其Value包含空或“#”表示无需鉴权，调用LoginHolder获取用户信息会抛异常
     * 2、测试环境 且 token为PC用户的ID 则 查询数据库获取用户信息，查不到用户的话调用LoginHolder获取用户信息会抛异常
     * 3、测试环境传真实token 或 正式环境，一律需校验token
     * @param token
     * @param tokenIsNumber
     * @return
     */
    private void dealPcRequest(String token, boolean tokenIsNumber,Method method) {

        //RolePermissions Value包含空或“#” 则无需鉴权
        RolePermissions rolePermissions = RequestUtil.getRolePermissionsAnnotation(method);
        //权限包含“”或“#”的直接放行
        if(rolePermissions != null){
            List<String> ctrCodes = Arrays.asList(rolePermissions.value());
            if( ctrCodes.contains("") || ctrCodes.contains("#") ){
                return;
            }
        }

        //如果是测试环境且是数字虚拟token,则直接调用PC用户详情
        if( isPcTest && tokenIsNumber ){
            SystemUser systemUser = systemUserMapper.selectById(Long.valueOf(token));
            if( systemUser != null ){
                SystemLoginDTO loginDTO = TransferUtil.changeToObj( systemUser, SystemLoginDTO.class );
                if (loginDTO != null) {
                    LoginHolder.setPcUser(loginDTO);
                    checkRolePermissions(rolePermissions,loginDTO);
                }
            }
        }else{
            //正式环境一律需真实token
            SystemLoginDTO loginDTO = CacheUtil.checkPcToken(token);
            Assert.isEmpty( loginDTO,ErrorCode.NO_LOGIN.reMsg() );
            LoginHolder.setPcUser(loginDTO);
            checkRolePermissions(rolePermissions,loginDTO);
        }
    }


    /**
     * 校验当前登录用户是否拥有注解上的指定权限
     * @param rolePermissions   RolePermissions注解
     */
    public void checkRolePermissions(RolePermissions rolePermissions){
        checkRolePermissions(rolePermissions,LoginHolder.getPcUser());
    }

    /**
     * 校验当前登录用户是否拥有注解上的指定权限
     * @param rolePermissions   RolePermissions注解
     */
    public void checkRolePermissions(RolePermissions rolePermissions,SystemLoginDTO pcUser){
        if(rolePermissions == null){return;}

        //获取当前管理员
        Assert.isEmpty(pcUser,"您暂无权限调用该接口");
        if( pcUser.getId().equals(1L) || pcUser.getRoleId().equals(1L) ){
            return;
        }

        //获取当前登陆人的权限
        List<String> codesList = CacheUtil.getPcRoleCodes(pcUser.getRoleId());
        //判断用户权限列表是否满足调用接口的权限
        String[] ctrCodes = rolePermissions.value();
        Boolean hasContains = codeMatch(codesList, ctrCodes, rolePermissions.isAnd());
        Assert.checkBoolean( hasContains ,ErrorCode.AUTH_ERR.reMsg("非法访问[" + ctrCodes.length + "]"));
    }

    /**
     * 处理不需要走业务逻辑的地址,如果满足以下配置路径，则跳过该拦截器,仅打印日志
     * @param uri
     * @return
     */
    public boolean dealExcludePathPatterns(String uri){
        for (String excludePathPattern : excludePathPatterns) {
            if( matcher.match(excludePathPattern,uri) ){
                return true;
            }
        }
        return false;
    }

    /**
     * 校验权限
     * @param roleCodes 当前角色拥有的权限
     * @param ctrCodes  接口需要的权限-数组
     * @param isAnd  匹配模式 true-全部满足 false-满足一个即可
     * @return  true-校验通过 false-校验未通过
     */
    public Boolean codeMatch(List<String> roleCodes,String[] ctrCodes,Boolean isAnd){
        for (String ctrCode : ctrCodes) {
            ctrCode = ctrCode.trim();
            if( "".equals(ctrCode) || "*".equals(ctrCode) || "#".equals(ctrCode) ){
                return true;
            }
            boolean isOk = false;
            for (String roleCode : roleCodes) {
                if(matcher.match(ctrCode, roleCode)){
                    isOk = true;
                    break;
                }
            }
            //checkOr
            if( !isAnd && isOk ){
                return true;
            }else if( isAnd && !isOk ){
                return false;
            }
        }
        return isAnd;
    }

    private static void res404(HttpServletResponse response) throws IOException {
        String body = JSONObject.toJSONString(B.fail(ErrorCode.REQUEST_NOT_EXISTS.reMsg()));
        response.setCharacterEncoding("UTF-8");
        response.setContentType(MediaType.APPLICATION_JSON.getValue());
        response.getWriter().write(body);
    }

}
